package Netty.权威指导demo.Http文件服务器;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.CharsetUtil;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;


/**
 * @author caihe
 * @date 2022/2/18 10:58
 */
public class HttpFileServer {


    public void run(final int port, final String url) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
                    ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
                    ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
                    ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                    ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url));
                }
            });

            ChannelFuture future = b.bind(port).sync();
            System.out.println("HTTP文件目录服务器启动，网址是：http://127.0.0.1:" + port + url);
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new HttpFileServer().run(8080, "/src");
    }

    class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {


        private final String url;

        public HttpFileServerHandler(String url) {
            this.url = url;
        }

        /**
         * 当服务器接收到消息时，会自动触发 messageReceived方法
         */
        @Override
        protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
            // 5.0.0  request.getDecoderRequest() 已经被弃用
            //对HTTP请求消息的解码结果进行判断，如果解码失败则返回400错误
            if (!request.decoderResult().isSuccess()) {
                System.out.println("解码失败返回400");
                sendError(ctx, HttpResponseStatus.BAD_REQUEST);
                return;
            }
            //对请求方法进行判断，如果不是从浏览器或者表单设置为GET发起的请求（例如POST），则返回405错误
            if (request.method() != HttpMethod.GET) {
                sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
                System.out.println("请求方式不是GET，返回405");
                return;
            }

            //如果URI不合法 返回403错误
            final String uri = request.uri();
            System.out.println("request.uri : " + uri);
            final String path = sanitizeUri(uri);
            System.out.println("path : " + path);
            if (path == null) {
                sendError(ctx, HttpResponseStatus.FORBIDDEN);
                System.out.println("URI不合法 返回403错误");
                return;
            }
            //如果文件不存在或者是系统隐藏文件 则返回404错误
            File file = new File(path);

            if (file.isHidden() || !file.exists()) {
                sendError(ctx, HttpResponseStatus.NOT_FOUND);
                System.out.println("文件不存在或者是系统隐藏文件 则返回404错误");
                return;
            }
            if (file.isDirectory()) {
                if (uri.endsWith("/")) {
                    //返回
                    sendListing(ctx, file);
                } else {
                    //重定向
                    sendRedirect(ctx, uri + "/");
                }
                return;
            }
            //判断文件合法性
            if (!file.isFile()) {
                sendError(ctx, HttpResponseStatus.FORBIDDEN);
                return;
            }
            RandomAccessFile randomAccessFile = null;
            try {
                //以只读方式打开文件
                randomAccessFile = new RandomAccessFile(file, "r");
            } catch (FileNotFoundException e) {
                sendError(ctx, HttpResponseStatus.NOT_FOUND);
                return;
            }
            //获取文件的长度构造成功的HTTP应答消息
            long fileLength = randomAccessFile.length();
            DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            HttpHeaderUtil.setContentLength(response, fileLength);
            setContentTypeHeader(response, file);
            //判断是否是keepAlive，如果是就在响应头中设置CONNECTION为keepAlive
            if (HttpHeaderUtil.isKeepAlive(request)) {
                response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
            }
            ctx.write(response);
            ChannelFuture sendFileFuture;
            //通过Netty的ChunkedFile对象直接将文件写入到发送缓冲区中
            sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
            //为sendFileFuture添加监听器，如果发送完成打印发送完成的日志
            sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
                @Override
                public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception {
                    System.out.println("传输完成.");

                }

                @Override
                public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) throws Exception {
                    if (total < 0) {
                        System.err.println("传输进度 : " + progress);
                    } else {
                        System.err.println("传输进度: " + progress + "/" + total);
                    }
                }
            });
            //如果使用chunked编码，最后需要发送一个编码结束的空消息体，将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区中，
            //来标示所有的消息体已经发送完成，同时调用flush方法将发送缓冲区中的消息刷新到SocketChannel中发送
            ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
            //如果是非keepAlive的，最后一包消息发送完成后，服务端要主动断开连接
            if (!HttpHeaderUtil.isKeepAlive(request)) {
                lastContentFuture.addListener(ChannelFutureListener.CLOSE);
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            if (ctx.channel().isActive()) {
                sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
            }
        }

        private final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

        /**
         * @param uri
         * @return
         */
        private String sanitizeUri(String uri) {
            //对URL进行解码 解码成功后对URI进行合法性判断 如果URI与允许访问的URI一直或者是其子目录（文件），则检验通过否则返回空
            try {
                uri = URLDecoder.decode(uri, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                try {
                    uri = URLDecoder.decode(uri, "ISO-8859-1");
                } catch (UnsupportedEncodingException e1) {
                    throw new Error();
                }
            }
            //解码成功后对uri进行合法性判断，避免访问无权限的目录
            if (!uri.startsWith("/src")) {
                System.out.println("uri不是以/src开头的,uri不合法！");
                return null;
            }
            if (!uri.startsWith("/")) {
                System.out.println("uri不是以/开头的");
                return null;
            }

            //将硬编码的文件路径分隔符替换为本地操作系统的文件路径分割符
            uri = uri.replace('/', File.separatorChar);
            //对新的URI做二次合法性校验，如果校验失败则直接返回空
            if (uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.startsWith(".") || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) {
                System.out.println("新的URI做二次合法性校验失败则直接返回空");
                return null;
            }
            //最后对文件进行拼接，使用当前运行程序所在的工作目录 + URI 构造绝对路径返回
            System.out.println("最后对文件进行拼接:" + System.getProperty("user.dir") + File.separator + uri);
            return System.getProperty("user.dir") + File.separator + uri;

        }

        private final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");

        /**
         * 发送目录的链接到客户端浏览器
         *
         * @param ctx
         * @param dir
         */
        private void sendListing(ChannelHandlerContext ctx, File dir) {
            //创建成功的http响应消息
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            //设置消息头的类型是html文件，不要设置为text/plain，客户端会当做文本解析
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
            //构造返回的html页面内容
            StringBuilder buf = new StringBuilder();
            String dirPath = dir.getPath();
            buf.append("<!DOCTYPE html>\r\n");
            buf.append("<html><head><title>");
            buf.append(dirPath);
            buf.append("目录：");
            buf.append("</title></head><body>\r\n");
            buf.append("<h3>");
            buf.append(dirPath).append("目录：");
            buf.append("</h3>\r\n");
            buf.append("<ul>");
            buf.append("<li>链接：<a href=\"../\">..</a></li>\r\n");
            for (File f : dir.listFiles()) {
                if (f.isHidden() || !f.canRead()) {
                    continue;
                }
                String name = f.getName();
                if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
                    continue;
                }
                buf.append("<li>链接：<a href=\"");
                buf.append(name);
                buf.append("\">");
                buf.append(name);
                buf.append("</a></li>\r\n");
            }
            buf.append("</ul></body></html>\r\n");
            System.out.println("buf ：" + buf);
            //分配消息缓冲对象
            ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
            //将缓冲区的内容写入响应对象，并释放缓冲区
            response.content().writeBytes(buffer);
            buffer.release();
            //将响应消息发送到缓冲区并刷新到SocketChannel中
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);

        }

        private void sendRedirect(ChannelHandlerContext ctx, String newUri) {
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
            response.headers().set(HttpHeaderNames.LOCATION, newUri);
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }

        private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }

        private void setContentTypeHeader(HttpResponse response, File file) {
            MimetypesFileTypeMap mimetypesTypeMap = new MimetypesFileTypeMap();
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesTypeMap.getContentType(file.getPath()));
        }
    }


}
